﻿/**************************************************************
*作者：Leon
*创建时间：2022/4/7 18:47:28
**************************************************************/
using Microsoft.Toolkit.Mvvm.ComponentModel;
using System.ComponentModel;

namespace Lad.DrawProject.Controls;

/// <summary>
/// Behavior flag for argument of ActionPerformmed event.
/// </summary>
public enum ActionBehavior
{
    /// <summary>
    /// Do action (action is firstly done)
    /// </summary>
    Do,

    /// <summary>
    /// Redo action (action is redone by ActionManager)
    /// </summary>
    Redo,

    /// <summary>
    /// Undo action (action is undone by ActionManager)
    /// </summary>
    Undo,
}

/// <summary>
/// Represents action interface.
/// </summary>
public interface IAction
{
    /// <summary>
    /// Do this action.
    /// </summary>
    void Do();

    /// <summary>
    /// Get the friendly name of this action.
    /// </summary>
    /// <returns>Get friendly name of action.</returns>
    string GetName();
}

/// <summary>
/// Undoable action interface.
/// </summary>
public interface IUndoableAction : IAction
{
    /// <summary>
    /// Redo this action.
    /// </summary>
    void Redo();

    /// <summary>
    /// Undo this action.
    /// </summary>
    void Undo();
}

internal interface ISerialUndoAction : IUndoableAction
{
}

/// <summary>
/// Action event argument.
/// </summary>
public class ActionEventArgs : EventArgs
{
    private IAction action;

    /// <summary>
    /// Construct an argument with specified action and behavior flag.
    /// </summary>
    /// <param name="action">action is currently performing.</param>
    /// <param name="behavior">behavior flag of current operation.</param>
    public ActionEventArgs(IAction action, ActionBehavior behavior)
    {
        this.action = action;
        this.Behavior = behavior;
    }

    /// <summary>
    /// The action is currently performing.
    /// </summary>
    public IAction Action
    {
        get { return action; }
        set { action = value; }
    }

    /// <summary>
    /// The behavior of current action performing. (one of do/undo/redo)
    /// </summary>
    public ActionBehavior Behavior { get; set; }

    /// <summary>
    /// Get or set the Cancel flag to decide whether or not to cancel this operation.
    /// </summary>
    public bool Cancel { get; set; }
}

/// <summary>
/// Action group is used to perform several actions together during one time operation,
/// For example there is two actions:
/// <ol>
/// <li>expend spreadsheet action</li>
/// <li>copy data action</li>
/// </ol>
/// Sometimes it is necessary to perform these two actions together, they are should undo 
/// together, in this case, create an ActionGroup and add them into the group, then invoke
/// the 'DoAction' method of 'ActionManager' by passing this action group object.
/// </summary>
public class ActionGroup : IUndoableAction
{
    private List<IAction> actions;

    private readonly string name;

    /// <summary>
    /// Construct action group by specified name, and the collection of action to perform together.
    /// </summary>
    /// <param name="name">Friendly name of this group.</param>
    /// <param name="actions">Collection of action to be performed.</param>
    public ActionGroup(string name, List<IAction> actions)
    {
        this.name = name;
        this.actions = actions;
    }

    /// <summary>
    /// Construct action group by specified name, and the collection of action to perform together.
    /// </summary>
    /// <param name="name">Friendly name of this group.</param>
    public ActionGroup(string name)
    {
        this.name=name;
        actions = new();
    }

    /// <summary>
    /// Action list stored in this group.
    /// </summary>
    public List<IAction> Actions
    {
        get { return actions; }
        set { actions = value; }
    }
    /// <summary>
    /// Do this action group. (Do all actions that are contained in this group)
    /// </summary>
    public virtual void Do()
    {
        foreach (IAction action in actions)
        {
            action.Do();
        }
    }

    /// <summary>
    /// Get the friendly name of this action group.
    /// </summary>
    /// <returns>Friendly name of this action.</returns>
    public virtual string GetName()
    {
        return name;
    }

    /// <summary>
    /// Redo this action group. (Redo all actions that are contained in this group)
    /// </summary>
    public virtual void Redo()
    {
        this.Do();
    }

    /// <summary>
    /// Convert this action group object into string for displaying.
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return string.Format("ActionGroup[" + name + "]");
    }

    /// <summary>
    /// Undo this action group. (Undo all actions that are contained in this group)
    /// </summary>
    public virtual void Undo()
    {
        for (int i = actions.Count - 1; i >= 0; i--)
            ((IUndoableAction)actions[i]).Undo();
    }
}

/// <summary>
/// Base action for all actions that are used for worksheet operations.
/// </summary>
public abstract class BaseAction : IUndoableAction
{

    /// <summary>
    /// Do this action.
    /// </summary>
    public abstract void Do();

    /// <summary>
    /// Get friendly name of this action.
    /// </summary>
    /// <returns>Get friendly name of this action.</returns>
    public abstract string GetName();

    /// <summary>
    /// Redo this action.
    /// </summary>
    public virtual void Redo()
    {
        this.Do();
    }

    /// <summary>
    /// Undo this action.
    /// </summary>
    public abstract void Undo();
}

public sealed class ActionManager : ObservableObject
{
    private static readonly string LOGKEY = "actionmanager";

    private readonly int capacity = 30;

    private readonly Stack<IUndoableAction> redoStack = new();

    private readonly List<IUndoableAction> undoStack = new();

    public ActionManager()
    {
    }
    public event EventHandler<ActionEventArgs>? AfterPerformAction;

    public event EventHandler<ActionEventArgs>? BeforePerformAction;

    /// <summary>
    /// Get collection of redo action list
    /// </summary>
    public Stack<IUndoableAction> RedoStack
    {
        get { return redoStack; }
    }

    /// <summary>
    /// Get collection of undo action list
    /// </summary>
    public List<IUndoableAction> UndoStack
    {
        get { return undoStack; }
    }
    public void AddAction(IAction action)
    {
        DoAction(action, false);
    }

    public bool CanRedo()
    {
        return redoStack.Count > 0;
    }

    public bool CanUndo()
    {
        return undoStack.Count > 0;
    }

    public void DoAction(IAction action)
    {
        DoAction(action, true);
    }

    #region Lambda
    class BaseAction : IUndoableAction
    {
        private readonly Action action;
        private readonly Action unDoAction;

        public void Do() => action();
        public string GetName() => " Lambda Action ";
        public void Redo() => this.Do();
        public void Undo() => unDoAction();
        public BaseAction(Action action, Action unDoAction)
        {
            this.action = action;
            this.unDoAction = unDoAction;
        }
    }
    public void DoAction(Action action, Action unDoAction)
    {
        DoAction(new BaseAction(action, unDoAction));
    }
    public void AddAction(Action action, Action unDoAction)
    {
        DoAction(new BaseAction(action, unDoAction), false);
    }
    #endregion

    public IAction? Redo()
    {
        if (redoStack.Count > 0)
        {
            IUndoableAction? action = null;

            while (redoStack.Count > 0)
            {
                action = redoStack.Pop();
                //Logger.Log(LOGKEY, "redo action: " + action.ToString());

                if (BeforePerformAction != null)
                {
                    var arg = new ActionEventArgs(action, ActionBehavior.Redo);
                    BeforePerformAction(this, arg);
                    if (arg.Cancel) break;
                }

                action.Redo();
                undoStack.Add(action);

                AfterPerformAction?.Invoke(this, new ActionEventArgs(action, ActionBehavior.Redo));

                if (action is not ISerialUndoAction) break;
            }
            OnPropertyChanged(nameof(RedoStack));
            return action;
        }

        OnPropertyChanged(nameof(RedoStack));
        return null;

    }

    /// <summary>
    /// Clear current action stack.
    /// </summary>
    public void Reset()
    {
        redoStack.Clear();
        undoStack.Clear();
    }

    public IAction? Undo()
    {
        if (undoStack.Count > 0)
        {
            IUndoableAction? action = null;

            while (undoStack.Count > 0)
            {
                action = undoStack.Last();
                //Logger.Log(LOGKEY, "undo action: " + action.ToString());

                // before event
                if (BeforePerformAction != null)
                {
                    var arg = new ActionEventArgs(action, ActionBehavior.Undo);
                    BeforePerformAction(this, new ActionEventArgs(action, ActionBehavior.Undo));
                    if (arg.Cancel) break;
                }

                undoStack.Remove(action);
                action.Undo();
                redoStack.Push(action);

                // after event
                AfterPerformAction?.Invoke(this, new ActionEventArgs(action, ActionBehavior.Undo));

                if (action is not ISerialUndoAction) break;
            }
            OnPropertyChanged(nameof(UndoStack));
            return action;
        }
        OnPropertyChanged(nameof(UndoStack));
        return null;
    }

    /// <summary>
    /// Do specified action.
    /// </summary>
    /// <param name="action">Action to be performed</param>
    /// <param name="perform">True to perform immediately, false to add into stack only</param>
    /// <param name="isCanUndo">Specifies that whether the action can be undone, 
    /// sometimes an action might not necessary to be undone even it implements the 
    /// IUndoable interface.</param>
    private void Do(IAction action, bool perform, bool isCanUndo)
    {
        //Logger.Log(LOGKEY, string.Format("{0} action: {1}[{2}]", perform ? "do" : "add", action.GetType().Name, action.GetName()));

        if (BeforePerformAction != null)
        {
            var arg = new ActionEventArgs(action, ActionBehavior.Do);
            BeforePerformAction(this, arg);
            if (arg.Cancel) return;
        }

        if (perform) action.Do();

        if (action is IUndoableAction undoAction && isCanUndo)
        {
            redoStack.Clear();
            undoStack.Add(undoAction);

            if (undoStack.Count > capacity)
            {
                int removedActions = undoStack.Count - capacity;
                undoStack.RemoveRange(0, removedActions);
                //Logger.Log(LOGKEY, "action stack full. remove " + (removedActions) + " action(s).");
            }
            OnPropertyChanged(nameof(RedoStack));
        }
        AfterPerformAction?.Invoke(this, new ActionEventArgs(action, ActionBehavior.Do));
        
    }

    private void DoAction(IAction action, bool perform)
    {
        Do(action, perform, (action is IUndoableAction));
    }
}
